/* * Copyright 2012 GitHub Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.mobile.ui.issue; import static android.app.Activity.RESULT_OK; import static android.view.View.GONE; import static android.view.View.VISIBLE; import static com.github.mobile.Intents.EXTRA_COMMENT; import static com.github.mobile.Intents.EXTRA_ISSUE; import static com.github.mobile.Intents.EXTRA_ISSUE_NUMBER; import static com.github.mobile.Intents.EXTRA_IS_COLLABORATOR; import static com.github.mobile.Intents.EXTRA_IS_OWNER; import static com.github.mobile.Intents.EXTRA_REPOSITORY_NAME; import static com.github.mobile.Intents.EXTRA_REPOSITORY_OWNER; import static com.github.mobile.Intents.EXTRA_USER; import static com.github.mobile.RequestCodes.COMMENT_CREATE; import static com.github.mobile.RequestCodes.COMMENT_DELETE; import static com.github.mobile.RequestCodes.COMMENT_EDIT; import static com.github.mobile.RequestCodes.ISSUE_ASSIGNEE_UPDATE; import static com.github.mobile.RequestCodes.ISSUE_CLOSE; import static com.github.mobile.RequestCodes.ISSUE_EDIT; import static com.github.mobile.RequestCodes.ISSUE_LABELS_UPDATE; import static com.github.mobile.RequestCodes.ISSUE_MILESTONE_UPDATE; import static com.github.mobile.RequestCodes.ISSUE_REOPEN; import static com.github.mobile.util.TypefaceUtils.ICON_COMMIT; import static org.eclipse.egit.github.core.service.IssueService.STATE_OPEN; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout.LayoutParams; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; import com.github.kevinsawicki.wishlist.ViewUtils; import com.github.mobile.R; import com.github.mobile.accounts.AccountUtils; import com.github.mobile.core.issue.FullIssue; import com.github.mobile.core.issue.IssueStore; import com.github.mobile.core.issue.IssueUtils; import com.github.mobile.core.issue.RefreshIssueTask; import com.github.mobile.ui.ConfirmDialogFragment; import com.github.mobile.ui.DialogFragment; import com.github.mobile.ui.DialogFragmentActivity; import com.github.mobile.ui.HeaderFooterListAdapter; import com.github.mobile.ui.SelectableLinkMovementMethod; import com.github.mobile.ui.StyledText; import com.github.mobile.ui.comment.CommentListAdapter; import com.github.mobile.ui.comment.DeleteCommentListener; import com.github.mobile.ui.comment.EditCommentListener; import com.github.mobile.ui.commit.CommitCompareViewActivity; import com.github.mobile.util.AvatarLoader; import com.github.mobile.util.HttpImageGetter; import com.github.mobile.util.ShareUtils; import com.github.mobile.util.ToastUtils; import com.github.mobile.util.TypefaceUtils; import com.google.inject.Inject; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Locale; import org.eclipse.egit.github.core.Comment; import org.eclipse.egit.github.core.Issue; import org.eclipse.egit.github.core.IssueEvent; import org.eclipse.egit.github.core.Label; import org.eclipse.egit.github.core.Milestone; import org.eclipse.egit.github.core.PullRequest; import org.eclipse.egit.github.core.Repository; import org.eclipse.egit.github.core.RepositoryId; import org.eclipse.egit.github.core.User; import retrofit.http.HEAD; /** * Fragment to display an issue */ public class IssueFragment extends DialogFragment { private int issueNumber; private List<Comment> comments; private List<Object> items; private RepositoryId repositoryId; private Issue issue; private User user; private boolean isCollaborator; private boolean isOwner; @Inject private AvatarLoader avatars; @Inject private IssueStore store; private ListView list; private ProgressBar progress; private View headerView; private View loadingView; private View footerView; private HeaderFooterListAdapter<CommentListAdapter> adapter; private EditMilestoneTask milestoneTask; private EditAssigneeTask assigneeTask; private EditLabelsTask labelsTask; private EditStateTask stateTask; private TextView stateText; private TextView titleText; private TextView bodyText; private TextView authorText; private TextView createdDateText; private ImageView creatorAvatar; private ViewGroup commitsView; private TextView assigneeText; private ImageView assigneeAvatar; private TextView labelsArea; private View milestoneArea; private View milestoneProgressArea; private TextView milestoneText; private MenuItem stateItem; @Inject private HttpImageGetter bodyImageGetter; @Inject private HttpImageGetter commentImageGetter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle args = getArguments(); repositoryId = RepositoryId.create( args.getString(EXTRA_REPOSITORY_OWNER), args.getString(EXTRA_REPOSITORY_NAME)); issueNumber = args.getInt(EXTRA_ISSUE_NUMBER); user = (User) args.getSerializable(EXTRA_USER); isCollaborator = args.getBoolean(EXTRA_IS_COLLABORATOR, false); isOwner = args.getBoolean(EXTRA_IS_OWNER, false); DialogFragmentActivity dialogActivity = (DialogFragmentActivity) getActivity(); milestoneTask = new EditMilestoneTask(dialogActivity, repositoryId, issueNumber) { @Override protected void onSuccess(Issue editedIssue) throws Exception { super.onSuccess(editedIssue); updateHeader(editedIssue); } }; assigneeTask = new EditAssigneeTask(dialogActivity, repositoryId, issueNumber) { @Override protected void onSuccess(Issue editedIssue) throws Exception { super.onSuccess(editedIssue); updateHeader(editedIssue); } }; labelsTask = new EditLabelsTask(dialogActivity, repositoryId, issueNumber) { @Override protected void onSuccess(Issue editedIssue) throws Exception { super.onSuccess(editedIssue); updateHeader(editedIssue); } }; stateTask = new EditStateTask(dialogActivity, repositoryId, issueNumber) { @Override protected void onSuccess(Issue editedIssue) throws Exception { super.onSuccess(editedIssue); updateHeader(editedIssue); } }; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); adapter.addHeader(headerView); adapter.addFooter(footerView); issue = store.getIssue(repositoryId, issueNumber); TextView loadingText = (TextView) loadingView .findViewById(R.id.tv_loading); loadingText.setText(R.string.loading_comments); if (issue == null || (issue.getComments() > 0 && items == null)) adapter.addHeader(loadingView); if (issue != null && items != null) updateList(issue, items); else { if (issue != null) updateHeader(issue); refreshIssue(); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.comment_list, null); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); list = finder.find(android.R.id.list); progress = finder.find(R.id.pb_loading); LayoutInflater inflater = getLayoutInflater(savedInstanceState); headerView = inflater.inflate(R.layout.issue_header, null); stateText = (TextView) headerView.findViewById(R.id.tv_state); titleText = (TextView) headerView.findViewById(R.id.tv_issue_title); authorText = (TextView) headerView.findViewById(R.id.tv_issue_author); createdDateText = (TextView) headerView .findViewById(R.id.tv_issue_creation_date); creatorAvatar = (ImageView) headerView.findViewById(R.id.iv_avatar); commitsView = (ViewGroup) headerView.findViewById(R.id.ll_issue_commits); assigneeText = (TextView) headerView.findViewById(R.id.tv_assignee_name); assigneeAvatar = (ImageView) headerView .findViewById(R.id.iv_assignee_avatar); labelsArea = (TextView) headerView.findViewById(R.id.tv_labels); milestoneArea = headerView.findViewById(R.id.ll_milestone); milestoneText = (TextView) headerView.findViewById(R.id.tv_milestone); milestoneProgressArea = headerView.findViewById(R.id.v_closed); bodyText = (TextView) headerView.findViewById(R.id.tv_issue_body); bodyText.setMovementMethod(SelectableLinkMovementMethod.getInstance()); loadingView = inflater.inflate(R.layout.loading_item, null); footerView = inflater.inflate(R.layout.footer_separator, null); commitsView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (IssueUtils.isPullRequest(issue)) openPullRequestCommits(); } }); stateText.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (issue != null) stateTask.confirm(STATE_OPEN.equals(issue.getState())); } }); milestoneArea.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (issue != null && isCollaborator) milestoneTask.prompt(issue.getMilestone()); } }); headerView.findViewById(R.id.ll_assignee).setOnClickListener( new OnClickListener() { @Override public void onClick(View v) { if (issue != null && isCollaborator) assigneeTask.prompt(issue.getAssignee()); } }); labelsArea.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (issue != null && isCollaborator) labelsTask.prompt(issue.getLabels()); } }); Activity activity = getActivity(); String userName = AccountUtils.getLogin(activity); adapter = new HeaderFooterListAdapter<>(list, new CommentListAdapter(activity.getLayoutInflater(), null, avatars, commentImageGetter, editCommentListener, deleteCommentListener, userName, isOwner, issue)); list.setAdapter(adapter); } private void updateHeader(final Issue issue) { if (!isUsable()) return; titleText.setText(issue.getTitle()); String body = issue.getBodyHtml(); if (!TextUtils.isEmpty(body)) bodyImageGetter.bind(bodyText, body, issue.getId()); else bodyText.setText(R.string.no_description_given); authorText.setText(issue.getUser().getLogin()); createdDateText.setText(new StyledText().append( getString(R.string.prefix_opened)).append(issue.getCreatedAt())); avatars.bind(creatorAvatar, issue.getUser()); if (IssueUtils.isPullRequest(issue) && issue.getPullRequest().getCommits() > 0) { ViewUtils.setGone(commitsView, false); TextView icon = (TextView) headerView.findViewById(R.id.tv_commit_icon); TypefaceUtils.setOcticons(icon); icon.setText(ICON_COMMIT); String commits = getString(R.string.pull_request_commits, issue.getPullRequest().getCommits()); ((TextView) headerView.findViewById(R.id.tv_pull_request_commits)).setText(commits); } else ViewUtils.setGone(commitsView, true); boolean open = STATE_OPEN.equals(issue.getState()); if (!open) { StyledText text = new StyledText(); text.bold(getString(R.string.closed)); Date closedAt = issue.getClosedAt(); if (closedAt != null) text.append(' ').append(closedAt); stateText.setText(text); } ViewUtils.setGone(stateText, open); User assignee = issue.getAssignee(); if (assignee != null) { StyledText name = new StyledText(); name.bold(assignee.getLogin()); name.append(' ').append(getString(R.string.assigned)); assigneeText.setText(name); assigneeAvatar.setVisibility(VISIBLE); avatars.bind(assigneeAvatar, assignee); } else { assigneeAvatar.setVisibility(GONE); assigneeText.setText(R.string.unassigned); } List<Label> labels = issue.getLabels(); if (labels != null && !labels.isEmpty()) { LabelDrawableSpan.setText(labelsArea, labels); labelsArea.setVisibility(VISIBLE); } else labelsArea.setVisibility(GONE); if (issue.getMilestone() != null) { Milestone milestone = issue.getMilestone(); StyledText milestoneLabel = new StyledText(); milestoneLabel.append(getString(R.string.milestone_prefix)); milestoneLabel.append(' '); milestoneLabel.bold(milestone.getTitle()); milestoneText.setText(milestoneLabel); float closed = milestone.getClosedIssues(); float total = closed + milestone.getOpenIssues(); if (total > 0) { ((LayoutParams) milestoneProgressArea.getLayoutParams()).weight = closed / total; milestoneProgressArea.setVisibility(VISIBLE); } else milestoneProgressArea.setVisibility(GONE); milestoneArea.setVisibility(VISIBLE); } else milestoneArea.setVisibility(GONE); String state = issue.getState(); if (state != null && state.length() > 0) state = state.substring(0, 1).toUpperCase(Locale.US) + state.substring(1); else state = ""; ViewUtils.setGone(progress, true); ViewUtils.setGone(list, false); updateStateItem(issue); } private void refreshIssue() { new RefreshIssueTask(getActivity(), repositoryId, issueNumber, bodyImageGetter, commentImageGetter) { @Override protected void onException(Exception e) throws RuntimeException { super.onException(e); ToastUtils.show(getActivity(), e, R.string.error_issue_load); ViewUtils.setGone(progress, true); } @Override protected void onSuccess(FullIssue fullIssue) throws Exception { super.onSuccess(fullIssue); if (!isUsable()) return; issue = fullIssue.getIssue(); comments = fullIssue; List<IssueEvent> events = (List<IssueEvent>) fullIssue.getEvents(); int numEvents = events.size(); List<Object> allItems = new ArrayList<>(); int start = 0; for (Comment comment : fullIssue) { for (int e = start; e < numEvents; e++) { IssueEvent event = events.get(e); if (comment.getCreatedAt().after(event.getCreatedAt())) { allItems.add(event); start++; } else { e = events.size(); } } allItems.add(comment); } // Adding the last events or if there are no comments for(int e = start; e < events.size(); e++) { IssueEvent event = events.get(e); allItems.add(event); } items = allItems; updateList(fullIssue.getIssue(), allItems); } }.execute(); } private void updateList(Issue issue, List<Object> items) { adapter.getWrappedAdapter().setItems(items); adapter.removeHeader(loadingView); adapter.getWrappedAdapter().setIssue(issue); headerView.setVisibility(VISIBLE); updateHeader(issue); } @Override public void onDialogResult(int requestCode, int resultCode, Bundle arguments) { if (RESULT_OK != resultCode) return; switch (requestCode) { case ISSUE_MILESTONE_UPDATE: milestoneTask.edit(MilestoneDialogFragment.getSelected(arguments)); break; case ISSUE_ASSIGNEE_UPDATE: assigneeTask.edit(AssigneeDialogFragment.getSelected(arguments)); break; case ISSUE_LABELS_UPDATE: ArrayList<Label> labels = LabelsDialogFragment .getSelected(arguments); if (labels != null && !labels.isEmpty()) labelsTask.edit(labels.toArray(new Label[labels.size()])); else labelsTask.edit(null); break; case ISSUE_CLOSE: stateTask.edit(true); break; case ISSUE_REOPEN: stateTask.edit(false); break; case COMMENT_DELETE: final Comment comment = (Comment) arguments.getSerializable(EXTRA_COMMENT); new DeleteCommentTask(getActivity(), repositoryId, comment) { @Override protected void onSuccess(Comment comment) throws Exception { super.onSuccess(comment); // Update comment list if (comments != null && comment != null) { int position = Collections.binarySearch(comments, comment, new Comparator<Comment>() { public int compare(Comment lhs, Comment rhs) { return Long.valueOf(lhs.getId()) .compareTo(rhs.getId()); } }); comments.remove(position); updateList(issue, items); } else refreshIssue(); } }.start(); break; } } private void updateStateItem(Issue issue) { if (issue != null && stateItem != null) if (STATE_OPEN.equals(issue.getState())) stateItem.setTitle(R.string.close).setIcon( R.drawable.menu_issue_close); else stateItem.setTitle(R.string.reopen).setIcon( R.drawable.menu_issue_open); } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); MenuItem editItem = menu.findItem(R.id.m_edit); MenuItem stateItem = menu.findItem(R.id.m_state); if (editItem != null && stateItem != null) { boolean isCreator = false; if(issue != null) isCreator = issue.getUser().getLogin().equals(AccountUtils.getLogin(getActivity())); editItem.setVisible(isOwner || isCollaborator || isCreator); stateItem.setVisible(isOwner || isCollaborator || isCreator); } updateStateItem(issue); } @Override public void onCreateOptionsMenu(Menu optionsMenu, MenuInflater inflater) { inflater.inflate(R.menu.issue_view, optionsMenu); stateItem = optionsMenu.findItem(R.id.m_state); updateStateItem(issue); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (RESULT_OK != resultCode || data == null) return; switch (requestCode) { case ISSUE_EDIT: Issue editedIssue = (Issue) data.getSerializableExtra(EXTRA_ISSUE); bodyImageGetter.encode(editedIssue.getId(), editedIssue.getBodyHtml()); updateHeader(editedIssue); return; case COMMENT_CREATE: Comment comment = (Comment) data .getSerializableExtra(EXTRA_COMMENT); if (items != null) { items.add(comment); issue.setComments(issue.getComments() + 1); updateList(issue, items); } else refreshIssue(); return; case COMMENT_EDIT: comment = (Comment) data .getSerializableExtra(EXTRA_COMMENT); if (comments != null && comment != null) { int position = Collections.binarySearch(comments, comment, new Comparator<Comment>() { public int compare(Comment lhs, Comment rhs) { return Long.valueOf(lhs.getId()).compareTo(rhs.getId()); } }); commentImageGetter.removeFromCache(comment.getId()); comments.set(position, comment); updateList(issue, items); } else refreshIssue(); return; } } private void shareIssue() { String id = repositoryId.generateId(); if (IssueUtils.isPullRequest(issue)) startActivity(ShareUtils.create("Pull Request " + issueNumber + " on " + id, "https://github.com/" + id + "/pull/" + issueNumber)); else startActivity(ShareUtils .create("Issue " + issueNumber + " on " + id, "https://github.com/" + id + "/issues/" + issueNumber)); } private void openPullRequestCommits() { if (IssueUtils.isPullRequest(issue)) { PullRequest pullRequest = issue.getPullRequest(); String base = pullRequest.getBase().getSha(); String head = pullRequest.getHead().getSha(); Repository repo = pullRequest.getBase().getRepo(); startActivity(CommitCompareViewActivity.createIntent(repo, base, head)); } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.m_edit: if (issue != null) startActivityForResult(EditIssueActivity.createIntent(issue, repositoryId.getOwner(), repositoryId.getName(), user), ISSUE_EDIT); return true; case R.id.m_comment: if (issue != null) startActivityForResult(CreateCommentActivity.createIntent( repositoryId, issueNumber, user), COMMENT_CREATE); return true; case R.id.m_refresh: refreshIssue(); return true; case R.id.m_share: if (issue != null) shareIssue(); return true; case R.id.m_state: if (issue != null) stateTask.confirm(STATE_OPEN.equals(issue.getState())); return true; default: return super.onOptionsItemSelected(item); } } /** * Edit existing comment */ final EditCommentListener editCommentListener = new EditCommentListener() { public void onEditComment(Comment comment) { startActivityForResult(EditCommentActivity.createIntent( repositoryId, issueNumber, comment, user), COMMENT_EDIT); } }; /** * Delete existing comment */ final DeleteCommentListener deleteCommentListener = new DeleteCommentListener() { public void onDeleteComment(Comment comment) { Bundle args = new Bundle(); args.putSerializable(EXTRA_COMMENT, comment); ConfirmDialogFragment.show( (DialogFragmentActivity) getActivity(), COMMENT_DELETE, getActivity() .getString(R.string.confirm_comment_delete_title), getActivity().getString( R.string.confirm_comment_delete_message), args); } }; }